﻿using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using NLog;

namespace MoneyWentWhere.Utils.Storage
{
    /// <summary>
    /// Encrypted and compressed wrapper for any other data format.
    /// Password must be specified.
    /// </summary>
    /// <typeparam name="DocT"></typeparam>
    public class EncryptedFormat<DocT> : FileFormat<DocT>
        where DocT : class, IHasStorageInfo
    {
        Logger Log = LogManager.GetCurrentClassLogger();

        public const String HeaderLine = "Encrypted DM-MWW";

        public readonly IDataFormat<DocT> InnerFormat;

        // NOTE: if I ever change this, I need to do more elaborate parsing 
        // matching within the stream when reading the file to strip off the header
        // (for backward compatibility)
        // TODO: add an optional second line to the header
        String FileHeader;

        public EncryptedFormat(IDataFormat<DocT> innerFormat, String fileHeader = HeaderLine)
        {
            innerFormat.ArgNotNull("innerFormat");
            fileHeader.ArgNotNull("fileHeader");

            InnerFormat = innerFormat;
            FileHeader = fileHeader;
        }

        public override DocT Load(Stream inStream, string password = null)
        {
            if (String.IsNullOrEmpty(password)) { throw new InvalidPasswordException("Null or empty password."); }

            DESCryptoServiceProvider des = new DESCryptoServiceProvider();

            byte[] key = Create64BitKey(password);
            des.Key = key;
            des.IV = key;

            // Read the file header
            if (!ContainsHeader(inStream))
            {
                throw new InvalidDataException("Encrypted file header not present.");
            }

            try
            {
                using (CryptoStream cryptoStream = new CryptoStream(inStream,
                    des.CreateDecryptor(), CryptoStreamMode.Read))
                {
                    using (GZipStream zipStream = new GZipStream(cryptoStream, CompressionMode.Decompress, true))
                    {
                        var doc = InnerFormat.Load(zipStream);
                        return doc;
                    }
                }
            }
            catch (CryptographicException e)
            {
                throw new InvalidPasswordException("Invalid password.", e);
            }
        }

        public override void Save(DocT document, Stream outStream)
        {
            document.StorageInfo.Password.ArgNotNullOrEmpty("doc.StorageInfo.Password");

            DESCryptoServiceProvider des = new DESCryptoServiceProvider();

            // Key must be 64-bit
            byte[] key = Create64BitKey(document.StorageInfo.Password);
            des.Key = key;
            des.IV = key;

            // Write the file header
            byte[] bh = Encoding.ASCII.GetBytes(FileHeader + "\r\n");
            outStream.Write(bh, 0, bh.Length);

            using (CryptoStream cryptoStream = new CryptoStream(outStream,
                des.CreateEncryptor(), CryptoStreamMode.Write))
            {
                using (GZipStream zipStream = new GZipStream(cryptoStream, CompressionMode.Compress))
                {
                    InnerFormat.Save(document, zipStream);
                }
            }
        }

        /// <summary>
        /// See if the file contains header.
        /// </summary>
        /// <returns></returns>
        public bool ContainsHeader(String filename)
        {
            try
            {
                using (var inStream = File.OpenRead(filename))
                {
                    return ContainsHeader(inStream);
                }
            }
            catch (Exception e)
            {
                Log.Warn("ContainsHeader error: " + e);
                return false;
            }
        }

        /// <summary>
        /// Check if the file contains the required format header
        /// </summary>
        /// <param name="inStream"></param>
        /// <returns></returns>
        bool ContainsHeader(Stream inStream)
        {
            // +2 for \r\n
            byte[] bh = new byte[FileHeader.Length + 2];
            inStream.Read(bh, 0, bh.Length);
            return ASCIIEncoding.ASCII.GetString(bh, 0, FileHeader.Length) == FileHeader;
        }

        static byte[] Create64BitKey(String password)
        {
            SHA1 hashFn = SHA1.Create();
            byte[] hash = hashFn.ComputeHash(UnicodeEncoding.Unicode.GetBytes(password));

            const int KeyLength = 8;
            byte[] key = new byte[KeyLength];
            Array.Copy(hash, key, KeyLength);
            return key;
        }
    }
}
